package jvs.vfs.ftp;
/*
*   Ftp.java
*
*   Li, Qiang
*   Copyright (C) 2000
*/

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

public class Ftp {
	private final static char PositivePreliminary  = '1';
	private final static char PositiveCompletion   = '2';
	private final static char PositiveIntermediate = '3';
	private final static char TransientNegative    = '4';
	//private final static char PermanentNegative    = '5';
	private String host = "localhost";
	private int port = 21;
	private boolean connected = false;
	//private Thread thread = null;
	private Socket commsock = null;
	private BufferedReader in = null;
	private BufferedWriter out = null;
	//private boolean versbose = false;
	//private String log = null;

	//private static final char separatorChar='/';

	public Ftp() {
		connected = open(host,port);
	}

	public Ftp(String host) {
		this.host = host;
		connected = open(host,port);
	}

	public Ftp(String host, int port) {
		this.host = host;
		this.port = port;
		connected = open(host,port);
	}

	synchronized public String ftpReply() {
		String reply = "500";
		try {
			reply = in.readLine();

			String code = reply.substring(0,3);
			boolean multiline = reply.substring(3,4).equals("-");
			while(multiline) {
				String line = in.readLine();
				multiline = !line.startsWith(code+" ");	
			}
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		return reply;
	}

	synchronized public void ftpCommand(String cmd) {
		//System.out.println("cmd: "+cmd);
		try {
			out.write(cmd+"\r\n");
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}	
	}

	synchronized public boolean open(String host, int port) {
		if(connected) {
			close();
		}
		try {
			commsock = new Socket(host,port);
			in =  new BufferedReader(new InputStreamReader(commsock.getInputStream()));
			out = new BufferedWriter(new OutputStreamWriter(commsock.getOutputStream()));
		}
		catch (UnknownHostException e) {
			return false;
		}
		catch (ConnectException e) {
			return false;
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		return ftpReply().startsWith("220");
	}

	synchronized public void close() {
		ftpCommand("BYE");
		try {
			in.close();
			out.close();
			commsock.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}

	synchronized public boolean login(String username, String password) {
		String reply;
		if(connected) {
			ftpCommand("REIN");
			reply = ftpReply();
		}
		else {
			if(!open(host,port)) {
				return false;
			}
		}
		ftpCommand("USER "+username);
		reply = ftpReply();
		if (reply.charAt(0) == PositiveIntermediate) {
			ftpCommand("PASS "+password);
			reply = ftpReply();
		}
		return (reply.charAt(0) == PositiveCompletion);
	}

	synchronized public void logout() {
		close();
	}

	synchronized public boolean cd(String dir) { 
		ftpCommand("CWD "+dir);
		return (ftpReply().charAt(0) == PositiveCompletion);
	}
		
	synchronized public boolean ascii() {
		ftpCommand("TYPE "+"A");
		return (ftpReply().charAt(0) == PositiveCompletion);
	}   
 
	synchronized public boolean binary() {
		ftpCommand("TYPE "+"I");
		return (ftpReply().charAt(0) == PositiveCompletion);
	}    

	private void copy(InputStream from, OutputStream to) {
		byte buf[] = new byte[1024];
		int len;
		try {
			while((len = from.read(buf,0,1024)) != -1) {
				to.write(buf,0,len);
			}
		} 
		catch(IOException e) {} 
	}

	synchronized public boolean get(String file, OutputStream os) {
		String datacomm = "RETR "+file;
		Socket datasock = getData(datacomm);
		try {
			InputStream is = datasock.getInputStream();
			copy(is,os);
			datasock.close();
			is.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean put(InputStream is, String file) {
		String datacomm = "STOR "+file;
		Socket datasock = getData(datacomm);
		try {
			OutputStream os = datasock.getOutputStream();
			copy(is,os);
			datasock.close();
			os.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		} 
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean put(String local, String remote) {
		FileInputStream data = null;
		try {
			data = new FileInputStream(local);
		}
		catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		String datacomm = "STOR "+remote;
		Socket datasock = getData(datacomm);
		try {
			OutputStream os = datasock.getOutputStream();
			copy(data,os);
			datasock.close();
			os.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		} 
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean rename(String from, String to) {
		ftpCommand("RNFR "+from);
		ftpCommand("RNTO "+to);
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean abort() {
		ftpCommand("ABOR ");
		String reply = ftpReply();
		if (reply.charAt(0) != TransientNegative) {
			reply = ftpReply();
		}
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean delete(String name) {
		ftpCommand("DELE "+name);
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean rmdir(String name) {
		ftpCommand("XRMD "+name);
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean mkdir(String name) {
		ftpCommand("XMKD "+name);
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public String pwd() {
		ftpCommand("PWD");
		String r = ftpReply();
		if (r.charAt(0) == PositiveCompletion) {
			int idx1 = r.indexOf('"')+1;
			int idx2 = r.lastIndexOf('"');
			String str = r.substring(idx1,idx2);
			StringTokenizer st = new StringTokenizer(str,"\"",true);
			String cwd = "";
			while (st.hasMoreTokens()) {
				String t = st.nextToken();
				cwd = cwd + t;
				if (t.equals("\"")) {
					if (!st.hasMoreTokens()) {
						break;
					}
					t = st.nextToken();
					if (t.equals("\"")) {
						continue;
					}
					cwd = cwd + t;
				}
			}
			return cwd;
		}
		return "/";
	}

	synchronized public String[] dir(String dir) { //list		
		String datacomm = "LIST "+dir;
		Socket datasock = getData(datacomm);
		Vector<String> list = new Vector<String>();
		String line;
		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(datasock.getInputStream()));
			while((line = br.readLine()) != null) {
				list.addElement(line);
			}
			datasock.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}

		Collections.sort(list,new NameComparator());
		String[] strs = new String[list.size()];

		Enumeration<String> e = list.elements(); 
		for (int i=0; e.hasMoreElements(); i++) {
			strs[i] = (String)e.nextElement();
		}
		return (ftpReply().charAt(0) == PositiveCompletion)? strs : null;
	}

	
	synchronized public String[] ls(String dir) { //nlist
		String datacomm = "NLST "+dir;	
		Socket datasock = getData(datacomm);
		Vector<String> list = new Vector<String>();
		String line;
		
		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(datasock.getInputStream()));
			while((line = br.readLine()) != null) {
				list.addElement(line);
			}
			datasock.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		
		Collections.sort(list);
		String[] strs = new String[list.size()];
		Enumeration<String> e=list.elements(); 
		for (int i=0; e.hasMoreElements(); i++) {
			strs[i] = (String)e.nextElement();
			int idx = strs[i].lastIndexOf("/");
			if (idx > 0) {
				strs[i] = strs[i].substring(idx+1);
			}
		}
		return (ftpReply().charAt(0) == PositiveCompletion)? strs : null;
	}

	synchronized public boolean system() {
		ftpCommand("SYST");
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public boolean noop() {
		ftpCommand("NOOP ");
		return (ftpReply().charAt(0) == PositiveCompletion);
	}

	synchronized public int size(String name) {
		String[] dir = dir(name);
		for(int i=0; i<dir.length; i++) {
			FileInfo info = new FileInfo(dir[i]);
			
			if(info.name.equals(".")     //name is directory
			|| info.name.equals(name)) { //name is file
				return Integer.parseInt(info.size);
			}	
		}
		return -1;
	}

	synchronized public boolean exists(String file) {
		return dir(file) != null;
	}

	synchronized public long date(String name) {
		String[] dir = dir(name);
		for(int i=0; i < dir.length; i++) {
			FileInfo info = new FileInfo(dir[i]);
			if(info.name.equals(".")     //name is directory
			|| info.name.equals(name)) { //name is file
				try {
					return DateFormat.getDateInstance().parse(info.date).getTime();
				} catch (ParseException pe) {
				} finally {
				}
			}	
		}
		return -1;
	}

	synchronized public OutputStream getOutputStream(String remote) {
		String datacomm = "STOR "+remote;
		Socket datasock = getData(datacomm);

		OutputStream os = null;
		try {
			os = datasock.getOutputStream();
		}
		catch (IOException e) {
			e.printStackTrace();
		} 
		//	
		checkReply();
		return os;
	}

	synchronized public InputStream getInputStream(String file) {
		String datacomm = "RETR "+file;
		Socket datasock = getData(datacomm);

		InputStream is = null;
		try {
			is = datasock.getInputStream();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		//
		checkReply();
		return is;
	}

	private Socket getData(String datacomm) {
		ServerSocket l = null;
		try {
			l = new ServerSocket(0);
		} 
		catch (IOException e) {
			e.printStackTrace();
		}
		int p = l.getLocalPort();
		InetAddress ia = commsock.getLocalAddress();

		String arg = ia.getHostAddress().replace('.',',')+","+p/256+","+p%256;
		ftpCommand("PORT "+arg);
		if (ftpReply().charAt(0) != PositiveCompletion) {
			return null;
		}
		ftpCommand(datacomm);
		switch (ftpReply().charAt(0)) {
		case PositivePreliminary:
		case PositiveCompletion:
			break;
		default:
			return null;
		}
		Socket datasock = null;
		try {
			datasock = l.accept();
			l.close();
		} 
		catch (IOException e) {
			e.printStackTrace();
		} 
		return datasock;
	}
	
	private void checkReply() {
		class Checker extends Thread {
			public Checker() {
				start();
			}
			public void run() {
				//We really don't care, just to consume
				//the reply.
				ftpReply();
			}
		}
		new Checker();
	}

	class FileInfo {
		public char type;
		public String permission; //rwxrwxrwx
		public String name;
		public String link;
		public String owner;
		public String group;
		public String size;
		public String date;
		public final static char DIRECTORY = 'd';
		public final static char FILE      = '-';
		public final static char LINK      = 'l';
		public final static char UNKNOWN   = '?';
		
		public FileInfo(String entry) {
			StringTokenizer st = new StringTokenizer(entry);
			if ( st.countTokens() != 9 && st.countTokens() != 11) {
				type = UNKNOWN;
				return;
			}
			String t = st.nextToken();
			type = t.charAt(0);
			permission = t.substring(1);
			st.nextToken(); //skip
			owner = st.nextToken();
			group = st.nextToken();
			size  = st.nextToken();
			date  = st.nextToken() + " " + st.nextToken() + " " + st.nextToken();
			name  = st.nextToken();
			if (name.equals(".") || name.equals("..")) {
				return;
			}
			while (st.hasMoreTokens()) {
				link = st.nextToken();
			}
		}
		
		public String toString() {
			StringBuffer sb = new StringBuffer();
			sb.append(permission);
			sb.append("\t");
			sb.append(group);
			sb.append("\t");
			sb.append(owner);
			sb.append("\t");
			sb.append(size);
			sb.append("\t");
			sb.append(date);
			sb.append("\t");
			sb.append(name);
			return sb.toString();
		}
	}

	class NameComparator implements Comparator<Object> {
		public int compare(Object o1, Object o2) {
			return compare((String)o1, (String)o2);
		}

		private int compare(String o1, String o2) {
			FileInfo e1 = new FileInfo(o1);
			FileInfo e2 = new FileInfo(o2);
			if (e1.type == FileInfo.UNKNOWN || e2.type == FileInfo.UNKNOWN) {
				return 0;
			}
			if (e1.type == e2.type) { 
				return e1.name.compareTo(e2.name);
			}
			if (e1.type == FileInfo.FILE) {
				return 1;
			}
			if (e1.type == FileInfo.DIRECTORY) {
				return -1;
			}
			if (e1.type == FileInfo.LINK && e2.type == FileInfo.FILE) {
				return -1;
			}
			return 1;
		}

		public boolean equals(Object o) {
			return (compare(this,o)==0);
		}
	}
}
/**/
